Skip to content

Conversation

@zensgit
Copy link
Owner

@zensgit zensgit commented Sep 19, 2025

Adds getTemplatesWithEtag(page/per_page/etag) to CategoryService. Computes weak ETag from last_updated+total, handles 304 by signaling notModified for caller-side cache.\nNext: wire into template dialogs to use ETag and paginated loading.

@Copilot Copilot AI review requested due to automatic review settings September 19, 2025 05:34
@gemini-code-assist
Copy link

Summary of Changes

Hello @zensgit, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces significant performance and user experience improvements to template management. It implements ETag-based caching and pagination on the backend API for template retrieval, reducing network load and enabling more efficient data handling. Concurrently, the Flutter frontend has been updated to leverage these new API capabilities, providing users with an enhanced template import dialog that supports selection, conflict resolution, and import previews.

Highlights

  • Backend API Enhancement: The backend API for fetching templates (/api/v1/templates/list) now supports pagination (page, per_page) and ETag-based caching (etag) to optimize data transfer and client-side caching.
  • ETag Generation and 304 Handling: The backend computes a weak ETag based on the maximum updated_at timestamp and the total count of templates matching the query. If the client provides a matching ETag, the API returns a 304 Not Modified status, avoiding redundant data transfer.
  • Frontend Template Import Dialog: The Flutter application's CategoryManagementEnhancedPage now includes a functional template import dialog. This dialog allows users to browse system templates, select multiple templates, define conflict resolution strategies (skip, rename, update), and preview the import results via a dry-run API call.
  • Client-side ETag and Pagination Integration: A new getTemplatesWithEtag method has been added to the CategoryService in Flutter, which interacts with the updated backend API. It handles 304 Not Modified responses and parses paginated template data, including computing client-side ETags.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces ETag-based caching and pagination for the template listing API, which is a great improvement for performance and client-side efficiency. The changes span both the Rust backend and the Flutter frontend.

My review has identified a few critical issues in the Rust implementation, including a potential panic and a bug that could lead to data inconsistency. There's also significant code duplication that should be addressed. On the Flutter side, the error handling for fetching templates needs to be improved to avoid a confusing user experience. I've also included some suggestions for code simplification.

Please address the critical issues before merging.

Comment on lines +163 to +168
if let Some(classification) = &params.r#type {
let classification = classification.to_lowercase();
if classification != "all" {
stats_q.push(" AND classification = ");
stats_q.push_bind(classification);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

It's correct to lowercase the classification here for a case-insensitive comparison. However, the same logic is missing when applying this filter to the main query builder (around line 139). This inconsistency can cause the total_count to mismatch the number of returned templates, which is a critical bug. Please ensure the filtering logic is identical in both places. This issue also highlights the risk of the duplicated filtering logic.

Comment on lines +187 to +188
let max_updated: chrono::DateTime<chrono::Utc> = stats_row.try_get("max_updated").unwrap_or(chrono::DateTime::<chrono::Utc>::from_timestamp(0, 0).unwrap());
let total_count: i64 = stats_row.try_get("total").unwrap_or(0);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

Using unwrap_or on a Result from try_get will panic if the operation fails (e.g., a column name typo or type mismatch), crashing the request handler. Please use unwrap_or_else to handle the error gracefully. It would also be beneficial to log the error inside the closure for easier debugging.

Suggested change
let max_updated: chrono::DateTime<chrono::Utc> = stats_row.try_get("max_updated").unwrap_or(chrono::DateTime::<chrono::Utc>::from_timestamp(0, 0).unwrap());
let total_count: i64 = stats_row.try_get("total").unwrap_or(0);
let max_updated: chrono::DateTime<chrono::Utc> = stats_row.try_get("max_updated").unwrap_or_else(|_| chrono::DateTime::<chrono::Utc>::from_timestamp(0, 0).unwrap());
let total_count: i64 = stats_row.try_get("total").unwrap_or_else(|_| 0);

Comment on lines +159 to +181
// Stats for ETag and total (duplicate the same filters)
let mut stats_q = sqlx::QueryBuilder::new(
"SELECT COALESCE(MAX(updated_at), to_timestamp(0)) AS max_updated, COUNT(*) AS total FROM system_category_templates WHERE is_active = true"
);
if let Some(classification) = &params.r#type {
let classification = classification.to_lowercase();
if classification != "all" {
stats_q.push(" AND classification = ");
stats_q.push_bind(classification);
}
}
if let Some(group) = &params.group {
stats_q.push(" AND category_group = ");
stats_q.push_bind(group);
}
if let Some(featured) = params.featured {
stats_q.push(" AND is_featured = ");
stats_q.push_bind(featured);
}
if let Some(since) = &params.since {
stats_q.push(" AND updated_at > ");
stats_q.push_bind(since);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This block for building the stats_q query duplicates the filtering logic from lines 136-157. This violates the DRY (Don't Repeat Yourself) principle and is error-prone, as demonstrated by the inconsistent classification handling mentioned in another comment. Any future changes to filtering logic will need to be applied in two places, increasing maintenance overhead. Please consider refactoring to a helper function or another pattern to apply filters to both QueryBuilder instances from a single source of truth.

Comment on lines +53 to +54
templates = await CategoryServiceIntegrated().getAllTemplates(forceRefresh: true);
} catch (_) {}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This empty catch block silently swallows any errors that occur while fetching templates. This leads to a poor user experience, as the user will be shown an empty import dialog without any indication of what went wrong. The error should be caught and communicated to the user, for example, by showing a SnackBar, and the loading indicator should be correctly handled.

Suggested change
templates = await CategoryServiceIntegrated().getAllTemplates(forceRefresh: true);
} catch (_) {}
templates = await CategoryServiceIntegrated().getAllTemplates(forceRefresh: true);
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error loading templates: $e')));
}
setState(() { _busy = false; });
return;
}

Comment on lines +105 to +110
/// 获取所有系统分类模板
Future<List<SystemCategoryTemplate>> getAllTemplates({
bool forceRefresh = false,
}) async {
return getSystemTemplates();
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The getAllTemplates method is a redundant wrapper around getSystemTemplates. It calls getSystemTemplates() with no arguments, which is behavior that getSystemTemplates already supports. This adds an unnecessary layer of indirection. To simplify the service's API, consider removing this method and calling getSystemTemplates() directly where needed.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Adds ETag-aware, paginated template listing to the API and wires up a minimal UI flow to import templates, with a Flutter service method to compute and handle weak ETags client-side.

  • Backend: Add page/per_page/etag query support, compute weak ETag from MAX(updated_at)+COUNT, return 304 on match, and paginate results.
  • Flutter service: Add getTemplatesWithEtag and TemplateCatalogResult; compute weak ETag; parse responses.
  • UI: Minimal “Import from template library” dialog flow (select, preview via dry-run, import), plus minor refactors.

Reviewed Changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 7 comments.

File Description
jive-flutter/lib/services/api/category_service.dart Adds ETag+pagination-aware fetch, weak ETag computation, and a result wrapper; moves SystemCategoryTemplate to models.
jive-flutter/lib/screens/management/category_management_enhanced.dart Implements a minimal template import dialog (selection, conflict strategy, preview, import).
jive-api/src/handlers/template_handler.rs Adds page/per_page/etag query params, computes ETag and 304 path, paginates the SQL, and returns last_updated from MAX(updated_at).

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Comment on lines +106 to +108
Future<List<SystemCategoryTemplate>> getAllTemplates({
bool forceRefresh = false,
}) async {
Copy link

Copilot AI Sep 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The forceRefresh parameter is unused; this method just proxies to getSystemTemplates(). Either remove the parameter or implement refresh behavior (e.g., bypass local cache) to avoid confusion.

Suggested change
Future<List<SystemCategoryTemplate>> getAllTemplates({
bool forceRefresh = false,
}) async {
Future<List<SystemCategoryTemplate>> getAllTemplates() async {

Copilot uses AI. Check for mistakes.
);

if (resp.statusCode == 304) {
return TemplateCatalogResult(const [], etag, true, 0, page, perPage);
Copy link

Copilot AI Sep 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On 304, returning items as [] and total as 0 can mislead consumers that don't check notModified. Consider making total nullable (int?) or adding a dedicated factory/flag behavior so callers can reliably differentiate 'unknown/unchanged' from 'empty/zero'. At minimum, document that items and total should be ignored when notModified is true.

Suggested change
return TemplateCatalogResult(const [], etag, true, 0, page, perPage);
return TemplateCatalogResult(const [], etag, true, null, page, perPage);

Copilot uses AI. Check for mistakes.
@@ -1,60 +1,184 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../models/category.dart';
Copy link

Copilot AI Sep 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This import appears unused in this file. Remove it to keep imports clean.

Suggested change
import '../../models/category.dart';

Copilot uses AI. Check for mistakes.
'per_page': perPage,
if (group != null) 'group': group,
if (classification != null)
'type': classification.toString().split('.').last,
Copy link

Copilot AI Sep 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer using the enum's name getter instead of string-splitting: 'type': classification.name. It's more robust and readable.

Suggested change
'type': classification.toString().split('.').last,
'type': classification.name,

Copilot uses AI. Check for mistakes.
Comment on lines 215 to +219
let response = TemplateResponse {
templates,
version: "1.0.0".to_string(),
last_updated: chrono::Utc::now().to_rfc3339(),
total: total.0,
last_updated: max_updated.to_rfc3339(),
total: total_count,
Copy link

Copilot AI Sep 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider surfacing the computed ETag in successful responses (e.g., include an etag field in TemplateResponse or set the ETag header) to help clients avoid re-computing it. Returning the header is preferable for intermediaries and caches.

Copilot uses AI. Check for mistakes.
Comment on lines +159 to +181
// Stats for ETag and total (duplicate the same filters)
let mut stats_q = sqlx::QueryBuilder::new(
"SELECT COALESCE(MAX(updated_at), to_timestamp(0)) AS max_updated, COUNT(*) AS total FROM system_category_templates WHERE is_active = true"
);
if let Some(classification) = &params.r#type {
let classification = classification.to_lowercase();
if classification != "all" {
stats_q.push(" AND classification = ");
stats_q.push_bind(classification);
}
}
if let Some(group) = &params.group {
stats_q.push(" AND category_group = ");
stats_q.push_bind(group);
}
if let Some(featured) = params.featured {
stats_q.push(" AND is_featured = ");
stats_q.push_bind(featured);
}
if let Some(since) = &params.since {
stats_q.push(" AND updated_at > ");
stats_q.push_bind(since);
}
Copy link

Copilot AI Sep 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The filters are duplicated for stats and data queries, causing two scans. Consider a single CTE or subquery to compute rows once, e.g., WITH filtered AS (...) SELECT COUNT(*), MAX(updated_at) FROM filtered; and SELECT ... FROM filtered ORDER BY ... LIMIT/OFFSET. This reduces DB work and avoids filter-drift bugs.

Copilot uses AI. Check for mistakes.
Comment on lines +340 to +345
/// 模板目录结果(含 ETag)
class TemplateCatalogResult {
final List<SystemCategoryTemplate> items;
final String? etag;
final bool notModified;
final int total;
Copy link

Copilot AI Sep 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please document the semantics when notModified is true (e.g., items/total should be ignored and callers should use their cache). Given 304 returns use total=0, explicit docs help prevent misuse.

Suggested change
/// 模板目录结果(含 ETag)
class TemplateCatalogResult {
final List<SystemCategoryTemplate> items;
final String? etag;
final bool notModified;
final int total;
/// 模板目录结果(含 ETag)
///
/// When [notModified] is true, this indicates that the data has not changed since the last request (e.g., HTTP 304).
/// In this case, [items] and [total] should be ignored, and callers should use their cached data instead.
/// Only [etag], [notModified], [page], [perPage], and [error] are meaningful when [notModified] is true.
class TemplateCatalogResult {
/// The list of category templates.
///
/// When [notModified] is true, this value should be ignored and callers should use their cached items.
final List<SystemCategoryTemplate> items;
/// The ETag value for cache validation.
final String? etag;
/// Whether the data was not modified (e.g., HTTP 304).
///
/// If true, [items] and [total] should be ignored and callers should use their cached data.
final bool notModified;
/// The total number of items.
///
/// When [notModified] is true, this value should be ignored and callers should use their cached total.
final int total;

Copilot uses AI. Check for mistakes.
@zensgit zensgit merged commit 1095be6 into main Sep 19, 2025
2 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant